using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace TfsSpam.Differ
{
    /// <summary>
    /// The FileDifference class represents one difference between two
    /// files. Information is provided about where in the files the
    /// difference is located and the actual lines of text that correspond
    /// to the difference.
    /// </summary>
    internal class FileDifference
    {
        #region -- Constructor
        /// <summary>
        /// Standard constructor for the class. Extracts the lines that represent
        /// the file difference and makes them available through the public properties
        /// provided by the class.
        /// </summary>
        /// <param name="difference">The information about the difference.</param>
        /// <param name="oldFileLines">The lines of text in the old file.</param>
        /// <param name="newFileLines">The lines of text in the new file.</param>
        /// <param name="contextLines">The number of lines of context that should be provided for the difference.</param>
        /// <param name="highlightPrefixes">The list of prefixes recognized for code highlighting.</param>
        public FileDifference( Diff.Item difference, string[] oldFileLines, string[] newFileLines, uint contextLines, List<string> highlightPrefixes )
        {
            NumberOfDeletedLines = difference.deletedA;
            NumberOfInsertedLines = difference.insertedB;
            OldFileStartLine = difference.StartA;
            NewFileStartLine = difference.StartB;

            GetDifferences( oldFileLines, newFileLines, contextLines, highlightPrefixes );
        }
        #endregion

        #region -- Public properties for basic difference information
        /// <summary>
        /// The number of lines deleted from the old file.
        /// </summary>
        public int NumberOfDeletedLines { get; private set; }

        /// <summary>
        /// The number of lines inserted into the new file.
        /// </summary>
        public int NumberOfInsertedLines { get; private set; }

        /// <summary>
        /// The line in the old file where the difference starts.
        /// </summary>
        public int OldFileStartLine { get; private set; }

        /// <summary>
        /// The line in the new file where the difference starts.
        /// </summary>
        public int NewFileStartLine { get; private set; }
        #endregion

        #region -- Support for merging file differences
        /// <summary>
        /// Determines if this <see cref="FileDifference"/> and the <see cref="Diff.Item"/> instance
        /// it represents overlaps with <paramref name="difference"/> given the specified number of
        /// context lines.
        /// </summary>
        /// <param name="difference">The <see cref="Diff.Item"/> instance to compare against.</param>
        /// <returns><c>true</c> if the two differences overlaps, <c>false</c> otherwise.</returns>
        public bool Overlaps( FileDifference difference )
        {
            DifferenceLineList ourDifferenceLines = new DifferenceLineList( Lines );
            DifferenceLineList theirDifferenceLines = new DifferenceLineList( difference.Lines );

            // If we already have the first line from the new file that the specified difference
            // refers to, there's an overlap between the two differences.
            return ( ourDifferenceLines.GetLine( LineSource.New, theirDifferenceLines.FirstLineFromNewFile.LineNumber ) != null );
        }

        /// <summary>
        /// Merges <paramref name="differenceToMerge"/> with this <see cref="FileDifference"/>
        /// instance.
        /// </summary>
        /// <remarks>
        /// Only <see cref="FileDifference"/> objects that overlap can be merged.
        /// </remarks>
        /// <param name="differenceToMerge">
        /// The <see cref="FileDifference"/> object that should be merged with this instance.
        /// </param>
        public void MergeDifference( FileDifference differenceToMerge )
        {
            DifferenceLineList ourLines = new DifferenceLineList( Lines );
            DifferenceLineList linesToMerge = new DifferenceLineList( differenceToMerge.Lines );

            // Update the type of all overlapping lines that we have
            // marked as context lines since they may have a different
            // type in the difference we're merging with.
            UpdateLineType( ourLines, linesToMerge );

            // Now add any deleted, added or context lines that we don't already have.
            MergeContextLinesBeforeDifference( ourLines, linesToMerge, differenceToMerge );
            MergeAddedLines( ourLines, linesToMerge );
            MergeContextLines( ourLines, linesToMerge );
            MergeDeletedLines( ourLines, linesToMerge, differenceToMerge );

            // Update the number of inserted/deleted lines we have after the merge.
            NumberOfInsertedLines = ourLines.AddedLines.Count;
            NumberOfDeletedLines = ourLines.DeletedLines.Count;

            // Update our list of lines.
            Lines = ourLines.AsReadOnly();
        }

        /// <summary>
        /// Updates the status of any context lines we have in our list of lines with
        /// the status from the corresponding lines in the difference we're merging
        /// with since our context lines may be lines that have changed.
        /// </summary>
        /// <param name="ourLines">The list of lines we already have.</param>
        /// <param name="linesToMerge">The list of lines from the difference we're merging with.</param>
        private static void UpdateLineType( DifferenceLineList ourLines, DifferenceLineList linesToMerge )
        {
            foreach ( DifferenceLine lineToMerge in linesToMerge )
            {
                DifferenceLine ourLine = ourLines.GetLineFromSameSource( lineToMerge );

                // Do we already have the line to merge in our list?
                if ( ourLine != null )
                {
                    if ( ourLine.LineType == LineType.Context )
                    {
                        // We have this line as a context line, since it may be something
                        // else in the difference we're merging with, merge this line.
                        ourLine.Merge( lineToMerge );
                    }
                }
            }
        }

        /// <summary>
        /// Merges the context lines above the change in the difference we're merging with
        /// with the lines we already have in our list of lines.
        /// </summary>
        /// <param name="ourLines">The list of lines we currently have.</param>
        /// <param name="linesToMerge">The list of lines from the difference we're merging with.</param>
        /// <param name="differenceToMerge">The difference we're merging with.</param>
        private static void MergeContextLinesBeforeDifference( DifferenceLineList ourLines, DifferenceLineList linesToMerge, FileDifference differenceToMerge )
        {
            List<DifferenceLine> contextLinesBeforeDifference = linesToMerge.GetLinesBefore( LineSource.New, differenceToMerge.NewFileStartLine );

            // Remove any context lines that we already have in our list.
            while ( contextLinesBeforeDifference.Count > 0 )
            {
                DifferenceLine currentLine = contextLinesBeforeDifference[ 0 ];

                if ( ourLines.GetLine( currentLine.LineSource, currentLine.LineNumber ) != null )
                {
                    // This line from the difference we're merging with already exists
                    // in our list of lines.
                    contextLinesBeforeDifference.RemoveAt( 0 );
                }
                else
                {
                    // We've reached the first context line we don't already have.
                    break;
                }
            }

            ourLines.AddRange( contextLinesBeforeDifference );
        }

        /// <summary>
        /// Adds any deleted lines from the difference we're merging with to our set of
        /// lines, making sure to place them at the appropriate location.
        /// </summary>
        /// <param name="ourLines">The list of lines we already have.</param>
        /// <param name="linesToMerge">The list of lines from the difference we're merging with.</param>
        /// <param name="differenceToMerge">The difference we're merging with.</param>
        private static void MergeDeletedLines( DifferenceLineList ourLines, DifferenceLineList linesToMerge, FileDifference differenceToMerge )
        {
            List<DifferenceLine> deletedMergeLines = linesToMerge.DeletedLines;

            // Do we have any deleted lines to merge?
            if ( deletedMergeLines.Count > 0 )
            {
                long newFileLineNumber = differenceToMerge.NewFileStartLine;
                DifferenceLine lastLineBeforeDeletedLines = ourLines.GetLine( LineSource.New, newFileLineNumber );

                if ( lastLineBeforeDeletedLines != null )
                {
                    int insertIndex = ourLines.IndexOf( lastLineBeforeDeletedLines );

                    // Add the deleted lines from the difference we're merging with after the
                    // line in the new file the deleted lines correspond to.
                    ourLines.InsertRange( insertIndex, deletedMergeLines );
                }
                else
                {
                    // The corresponding new line doesn't exist in our lines, we should add the deleted lines
                    // immediately after the last line we have.
                    ourLines.AddRange( deletedMergeLines );
                }
            }
        }

        /// <summary>
        /// Adds any added lines from the difference we're merging with to our set of
        /// lines, making sure to place them at the appropriate location.
        /// </summary>
        /// <param name="ourLines">The list of lines we already have.</param>
        /// <param name="linesToMerge">The list of lines from the difference we're merging with.</param>
        private static void MergeAddedLines( DifferenceLineList ourLines, DifferenceLineList linesToMerge )
        {
            List<DifferenceLine> addedMergeLines = linesToMerge.AddedLines;

            // Do we have any added lines to merge?
            if ( addedMergeLines.Count > 0 )
            {
                // Yes, remove any lines from the list of added lines that are already
                // present in our list of lines (the status of these lines will already
                // have been updated in the UpdateLineType method).
                while ( addedMergeLines.Count > 0 )
                {
                    DifferenceLine currentLine = addedMergeLines[ 0 ];

                    if ( ourLines.GetLine( currentLine.LineSource, currentLine.LineNumber ) != null )
                    {
                        // This line from the difference we're merging with already exists
                        // in our list of lines.
                        addedMergeLines.RemoveAt( 0 );
                    }
                    else
                    {
                        // We've reached the first added line we don't already have.
                        break;
                    }
                }

                ourLines.AddRange( addedMergeLines );
            }
        }

        /// <summary>
        /// Adds any context lines from the difference we're merging with to our set of
        /// lines, making sure to place them at the appropriate location.
        /// </summary>
        /// <param name="ourLines">The list of lines we already have.</param>
        /// <param name="linesToMerge">The list of lines from the difference we're merging with.</param>
        private static void MergeContextLines( DifferenceLineList ourLines, DifferenceLineList linesToMerge )
        {
            long ourLastNewFileLineNumber = ourLines.LastNewLineNumber;

            if ( ourLastNewFileLineNumber == DifferenceLineList.InvalidLineNumber )
            {
                throw new InvalidOperationException( "Could not merge context lines because there are now new file lines to merge with" );
            }
            else
            {
                List<DifferenceLine> contextLinesToAdd = linesToMerge.GetLinesAfter( LineSource.New, ourLastNewFileLineNumber );

                // Add all context lines after our last line from the new file from 
                // the difference we're merging with.
                ourLines.AddRange( contextLinesToAdd );
            }
        }
        #endregion

        #region -- Public properties for the actual lines that are different in the two files
        /// <summary>
        /// The lines from the two compared files that are relevant
        /// for this difference.
        /// </summary>
        public ReadOnlyCollection<DifferenceLine> Lines { get; private set; }
        #endregion

        #region -- Private initialization methods
        /// <summary>
        /// Retrieves the lines in the files being compared that are
        /// relevant for this difference.
        /// </summary>
        /// <param name="oldFileLines">The lines in the old file.</param>
        /// <param name="newFileLines">The lines in the new file.</param>
        /// <param name="contextLines">The number of context lines that should be included for the difference.</param>
        /// <param name="highlightPrefixes">The list of prefixes recognized for code highlighting.</param>
        private void GetDifferences( string[] oldFileLines, string[] newFileLines, uint contextLines, List<string> highlightPrefixes )
        {
            DifferenceLineList lines = new DifferenceLineList();

            lines.AddRange( GetLinesAboveDifference( newFileLines, contextLines ) );
            lines.AddRange( GetDeletedLines( oldFileLines ) );
            lines.AddRange( GetInsertedLines( newFileLines, highlightPrefixes ) );
            lines.AddRange( GetLinesBelowDifference( newFileLines, contextLines ) );

            Lines = lines.AsReadOnly();
        }

        /// <summary>
        /// Returns the lines just above the difference.
        /// </summary>
        /// <param name="fileLines">The lines in the file.</param>
        /// <param name="contextLines">The number of context lines to include for the difference.</param>
        /// <returns>
        /// A list containing 0 or up to <see cref="contextLines"/>
        /// <see cref="DifferenceLine"/> objects representing the lines
        /// just above the lines deleted from the old file.
        /// </returns>
        private DifferenceLineList GetLinesAboveDifference( string[] fileLines, uint contextLines )
        {
            long contextStart = Math.Max( 0, NewFileStartLine - contextLines );
            long contextEnd = NewFileStartLine;
            long numberOfLines = contextEnd - contextStart;

            return GetFileLines( fileLines, LineSource.New, LineType.Context, contextStart, numberOfLines );
        }

        /// <summary>
        /// Returns the lines just below the difference.
        /// </summary>
        /// <param name="fileLines">The lines in the file.</param>
        /// <param name="contextLines">The number of context lines to include for the difference.</param>
        /// <returns>
        /// A list containing 0 or up to <see cref="contextLines"/>
        /// <see cref="DifferenceLine"/> objects representing the lines
        /// just below the lines added to the new file.
        /// </returns>
        private DifferenceLineList GetLinesBelowDifference( string[] fileLines, uint contextLines )
        {
            long contextStart = Math.Min( fileLines.Length, NewFileStartLine + NumberOfInsertedLines );
            long contextEnd = Math.Min( fileLines.Length, contextStart + contextLines );
            long numberOfLines = contextEnd - contextStart;

            return GetFileLines( fileLines, LineSource.New, LineType.Context, contextStart, numberOfLines );
        }

        /// <summary>
        /// Returns the lines deleted from the old file.
        /// </summary>
        /// <param name="fileLines">The lines of text in the old file.</param>
        /// <returns>
        /// A list containing 0 or up to <see cref="NumberOfDeletedLines"/>
        /// <see cref="DifferenceLine"/> objects representing the lines deleted
        /// from the old file.
        /// </returns>
        private DifferenceLineList GetDeletedLines( string[] fileLines )
        {
            return GetFileLines( fileLines, LineSource.Old, LineType.Deleted, OldFileStartLine, NumberOfDeletedLines );
        }

        /// <summary>
        /// Returns the lines inserted into the new file.
        /// </summary>
        /// <param name="fileLines">The lines of text in the new file.</param>
        /// <param name="highlightPrefixes">The list of prefixes recognized for code highlighting.</param>
        /// <returns>
        /// A list containing 0 or up to <see cref="NumberOfInsertedLines"/>
        /// <see cref="DifferenceLine"/> objects representing the lines inserted
        /// into the new file.
        /// </returns>
        private DifferenceLineList GetInsertedLines( string[] fileLines, List<string> highlightPrefixes )
        {
            DifferenceLineList insertedLines = GetFileLines( fileLines, LineSource.New, LineType.Added, NewFileStartLine, NumberOfInsertedLines );

            // Process the code highlight prefixes for each inserted line.
            insertedLines.ForEach( delegate( DifferenceLine line ) { line.ProcessHighlightPrefixes( highlightPrefixes ); } );
            return insertedLines;
        }

        /// <summary>
        /// Retrieves the requested number of lines from the file, starting
        /// at the specified line.
        /// </summary>
        /// <remarks>
        /// Boundary checking is performed to ensure that only lines that are
        /// actually available are retrieved.
        /// </remarks>
        /// <param name="fileLines">The lines of text in the file.</param>
        /// <param name="source">The source of the lines.</param>
        /// <param name="type">The type of line being retrieved.</param>
        /// <param name="startLine">The index of the first line of text to retrieve.</param>
        /// <param name="numberOfLines">The number of lines of text to retrieve.</param>
        /// <returns>
        /// A list of 0 or up to <paramref name="numberOfLines"/> <see cref="DifferenceLine"/>
        /// objects depending on the lines available and the requested start line.
        /// </returns>
        static private DifferenceLineList GetFileLines( string[] fileLines, LineSource source, LineType type, long startLine, long numberOfLines )
        {
            long firstLineIndex = Math.Min( startLine, fileLines.Length );
            long lastLineIndex = Math.Min( startLine + numberOfLines, fileLines.Length );
            DifferenceLineList lines = new DifferenceLineList();

            for ( long lineIndex = firstLineIndex ; lineIndex < lastLineIndex ; lineIndex++ )
            {
                lines.Add( new DifferenceLine( fileLines[ lineIndex ], lineIndex, source, type ) );
            }

            return lines;
        }
        #endregion

        #region -- Base class overrides
        /// <summary>
        /// Overridden to provide actual difference information in
        /// the string representation.
        /// </summary>
        /// <remarks>
        /// The string starts with two @ signs, followed by the line number
        /// where the difference starts in the old file and the number of
        /// lines removed from the old file. The next segment gives the line
        /// number where the difference starts in the new file and the number
        /// of lines added to the new file before the string is ended with
        /// two @ signs.
        /// </remarks>
        public override string ToString()
        {
            return string.Format( System.Globalization.CultureInfo.InvariantCulture,  "@@ -{0},{1} +{2},{3} @@", OldFileStartLine, NumberOfDeletedLines, NewFileStartLine, NumberOfInsertedLines );
        }
        #endregion
    }
}
